Colorado River Atlas
  • Home
  • Reservoirs
  • State Usage
  • Snow Pack

On this page

  • Daily Snow Water Equivalent Trends
  • Current SWE Map
  • Annual Peak Average Snow Water Equivalent
  • Days to Peak Average SWE Each Year
SNOTEL = (await FileAttachment("Data/Snow Water Equivalent.csv").csv()).map(d => {
    const date  = new Date(d.Snow_Date + "T00:00:00")
    const month = date.getUTCMonth()

    return {
        ...d,
        Date: date, 
        Normalized_Date: new Date(Date.UTC(
            month >= 9 ? 2000 : 2001,
            month,
            date.getUTCDate()
        )),
        Calendar_Year: d.Calendar_Year,
        Snow_Year: d.Snow_Year,
        HUC6: d.HUC6,
        Minimum: +d.Min,
        Maximum: +d.Max,
        Median_91_20: +d.Median_91_20,
        Average_SWE: +d.Average_SWE
        }
}).sort((a, b) => a.Normalized_Date - b.Normalized_Date)
SNOTEL_Filtered = SNOTEL.filter(d => d.HUC6 === Basin_Select)
viewof Basin_Select = Inputs.select(Array.from(new Set(SNOTEL.map(d => d.HUC6))).sort(),
  { label: "Basin:", value: "Full Basin" })
HUC6_geo = FileAttachment("../../GIS_Data/SWE_HUC6.geojson").json()

Basin_States = FileAttachment("../../GIS_Data/Basin_States.geojson").json()

Daily Snow Water Equivalent Trends

{
    const current_wy  = "2026"
    const previous_wy = "2025"

    const Current_Year  = SNOTEL_Filtered.filter(d => d.Snow_Year === current_wy)
    // Use previous year to get a full year's worth of records for min, max and median
    const Previous_Year = SNOTEL_Filtered.filter(d => d.Snow_Year === previous_wy)
    const Historical    = SNOTEL_Filtered.filter(d => d.Snow_Year !== current_wy)

    return Plot.plot({
        //title: "Basin-Wide Average Snow Water Equivalent Trends",
        width: width,
        height: 500,
        color: { // For some reason adding the legend is causing the multi-year lines to disappear
            legend: true,
            domain: ["Current Water Year", "Median 1991-2020", "Minimum", "Maximum", "Previous Years"],
            range:  ["black", "#00c01a", "#f54242", "#3765fa", "#aaaaaa"]
            },
        x: {
             domain: [
                new Date(Date.UTC(2000, 9, 1)),    // October 1 2000
                new Date(Date.UTC(2001, 8, 30))    // September 30 2001
            ],
            tickFormat: "%b",
            line: true,
            label: "Month"
        },
        y: { nice: true, grid: true, zero: true, label: "Snow Water Equivalent (in)" },
        marks: [
            Plot.line(Historical, {
                x: "Normalized_Date",
                y: "Average_SWE",
                stroke: "#aaaaaa",
                z: ({ Date }) => Date.getUTCFullYear(), // z separates the lines
                strokeOpacity: 0.3
                }),
            Plot.line(Previous_Year, {
                x: "Normalized_Date",
                y: "Median_91_20",
                stroke: "#00c01a",
                strokeWidth: 2,
                tip: {
                    pointer: "xy",
                    maxRadius: 8,
                    format: {
                        x: d => d3.utcFormat("%b %d")(d)
                    }
                }
            }),
            Plot.line(Previous_Year, {
                x: "Normalized_Date",
                y: "Minimum",
                stroke: "#f54242",
                strokeWidth: 3,
                tip: {
                    pointer: "xy",
                    maxRadius: 8,
                    format: {
                        x: d => d3.utcFormat("%b %d")(d)
                    }
                }
            }),
            Plot.line(Previous_Year, {
                x: "Normalized_Date",
                y: "Maximum",
                stroke: "#3765fa",
                strokeWidth: 3,
                tip: {
                    pointer: "xy",
                    maxRadius: 8,
                    format: {
                        x: d => d3.utcFormat("%b %d")(d)
                    }
                }
            }),
            Plot.line(Current_Year, {
                x: "Normalized_Date",
                y: "Average_SWE",
                stroke: "#000000",
                strokeWidth: 3,
                tip: {
                    pointer: "xy",
                    maxRadius: 8,
                    format: {
                        x: d => d3.utcFormat("%b %d")(d)
                    }
                }
            })
        ]
    })
}

Current SWE Map

{
  const selected = Basin_Select

  return Plot.plot({
    width:  width,
    height: 500,
    marginLeft: 25,
    projection: {
      type: "albers",
      domain: HUC6_geo    // auto-fits projection to data extent
    },
    color: {
      legend:      true,
      marginLeft:  25,
      label:       "Avg SWE (in)",
      domain:      [0, 30],
      interpolate: d3.interpolateRdYlGn
    },
    marks: [
      // Filled polygons colored by SWE value
      Plot.geo(HUC6_geo, {
        fill:        d => d.properties.Average_SWE,
        fillOpacity: d => selected === "Full Basin"
          ? 0.7
          : d.properties.name === selected ? 0.9 : 0.15,
        stroke:      d => d.properties.name === selected ? "#000000" : "#504f4f",
        strokeWidth: d => d.properties.name === selected ? 2 : 0.75
      }),
      Plot.geo(Basin_States, {
        fillOpacity: 0,
        stroke: "#0000007c",
        strokeWidth: 2,
        tip: false
      }),
      Plot.tip(
        HUC6_geo.features,
        Plot.pointer(Plot.centroid({
        title:    d => `${d.properties.name}\n${d.properties.Average_SWE} in`,
        geometry: d => d.geometry
            }))
        ),
      Plot.frame()
    ]
  })
}
L = require("leaflet")
{
  const div = html`<div style="width:100%; height:700px;"></div>`

  // Allow div to render before initializing map
  yield div

  const map = L.map(div, { 
    center: [36.5, -112], 
    zoom: 6 
  })

    // Basemap tile layer
  const tiles = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
    attribution: "© OpenStreetMap contributors",
    maxZoom: 18
  }).addTo(map)

  // Color scale based on Average SWE
  const color_scale = d3.scaleSequential()
  .domain([ 0, 30 // Setting top end of color gradient to 30inches to extend just beyond maximum values observed in past
  ])
  .interpolator(d3.interpolateRdYlGn)

  // HUC6 layer
  const average_swe_layer = L.geoJSON(HUC6_geo, {
    style: feature => ({
      color: "#000000",
      weight: 2,
      fillColor: color_scale(feature.properties.Average_SWE),
      fillOpacity: 0.4
    }),
    onEachFeature: (feature, layer) => {
        layer.bindTooltip(
            `${feature.properties.name}: ${feature.properties.Average_SWE}in`,
            { sticky: true}
        )
    }
  }).addTo(map)

  const legend = L.control({ position: "bottomleft" })

  legend.onAdd = function (map) {
    var div = L.DomUtil.create('div', 'info legend');
    // White background
    div.style.cssText = `
        background: white; padding: 10px 14px;
        border-radius: 6px; border: 1px solid #dee2e6;
        font-size: 12px; font-family: sans-serif;
        `
    // Use CSS to create a smooth gradient bar
    div.innerHTML = '<div style="background: linear-gradient(to left, green, yellow, red); height: 20px; width: 200px;"></div>' +
                    '<div style="display: flex; justify-content: space-between;"><span>0in</span><span>30in</span></div>';
    return div;
  };

  legend.addTo(map);

  // Force Leaflet to recalculate map size after render
  setTimeout(() => map.invalidateSize(), 100)

}

Annual Peak Average Snow Water Equivalent

// Get the max average SWE for each HUC each year
yearly_max = {
  const rolled = d3.rollups(
    SNOTEL_Filtered,
    v => d3.max(v, d => d.Average_SWE),
    d => d.Snow_Year
  )
  return rolled.map(([year, max_avg]) => ({
    Year:        year,
    Date:        new Date(year, 1, 1),
    Average_SWE: max_avg
  })).sort((a, b) => a.Date - b.Date).filter(d => d.Date >= new Date("1979-01-01"))
}
{
    const valid = SNOTEL_Filtered.filter(d => d.Average_SWE != null && !isNaN(d.Average_SWE))
    const Date_Domain =  [d3.min(valid, d => d.Date), d3.max(valid, d => d.Date)]

    return Plot.plot({
        //title: "Basin-Wide Average Snow Water Equivalent Trends",
        width: width,
        height: 500,
        color: { 
            legend: true,
            domain: ["Annual Peak Avg SWE", "Peak Avg SWE 10yr-Avg", "Trend Line"],
            range: ["#0049b8", "steelblue", "black"]
        },
        x: { label: "Water Year", domain: Date_Domain },
        y: { nice: true, grid: true, zero: true, label: "Snow Water Equivalent (in)" },
        marks: [
            Plot.line(yearly_max, {
                x: "Date",
                y: "Average_SWE",
                stroke: "#0049b8",
                strokeOpacity: 1,
                channels: {
                    "Year":      { value: d => d.Year },
                    "SWE (in)":  { value: d => d.Average_SWE }
                },
                tip: {
                    pointer: "xy",
                    maxRadius: 8,
                    format: {
                        x: false,
                        y: false,
                        y1: false,
                        y2: false,
                        fill: false,
                        "Year": true,
                        "SWE (in)": true
                    }
                }
            }),
            Plot.line(yearly_max, Plot.windowY(
                { k: 10, reduce: "mean", anchor: "end" },
                { x: "Date", y: "Average_SWE", stroke: "steelblue", strokeDasharray: "4,4" }
            )),
            Plot.linearRegressionY(yearly_max, {x: "Date", y: "Average_SWE"})
        ]
    })
}

Days to Peak Average SWE Each Year

Days2Peak_Data = (await FileAttachment("Data/Days_to_Peak.csv").csv()).map(d => ({
    ...d,
    Date: new Date(d.Date + "T00:00:00"), 
    Peak_Avg_SWE: +d.Peak_AVG_SWE,
    HUC6: d.HUC6,
    Days2Peak: +d.Days2Peak,
    Snow_Year: d.Snow_Year
})).sort((a, b) => a.Date - b.Date)
{
    const Days2Peak = Days2Peak_Data.filter(d => d.HUC6 === Basin_Select)

    const valid = SNOTEL_Filtered.filter(d => d.Average_SWE != null && !isNaN(d.Average_SWE))
    const Date_Domain =  [d3.min(valid, d => d.Date), d3.max(valid, d => d.Date)]

    return Plot.plot({
        //title: "Basin-Wide Average Snow Water Equivalent Trends",
        width: width,
        height: 500,
        color: { 
            legend: true,
            domain: ["Days to Peak Avg SWE", "Days to Peak 10yr-Avg", "Trend Line"],
            range: ["#0049b8", "steelblue", "black"] 
        },
        x: { label: "Water Year", domain: Date_Domain },
        y: { grid: true, label: "Number of Days",
             domain: [d3.min(Days2Peak, d => d.Days2Peak) * 0.9, d3.max(Days2Peak, d => d.Days2Peak) * 1.1]
         },
        marks: [
            Plot.line(Days2Peak, {
                x: "Date",
                y: "Days2Peak",
                stroke: "#0049b8",
                strokeOpacity: 1,
                channels: {
                    "Year":      { value: d => d.Snow_Year },
                    "Days to Peak SWE":  { value: d => d.Days2Peak }
                },
                tip: {
                    pointer: "xy",
                    maxRadius: 8,
                    format: {
                        x: false,
                        y: false,
                        y1: false,
                        y2: false,
                        fill: false,
                        "Year": true,
                        "Days to Peak SWE": true
                    }
                }
            }),
            Plot.line(Days2Peak, Plot.windowY(
                { k: 10, reduce: "mean", anchor: "end" },
                { x: "Date", y: "Days2Peak", stroke: "steelblue", strokeDasharray: "4,4" }
            )),
            Plot.linearRegressionY(Days2Peak, {x: "Date", y: "Days2Peak"})
        ]
    })
}